

namespace Profit
{
    using System;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Diagnostics;

    public sealed class Win32Native
    {
        /// <summary>
        /// Folder browser dialog wrapper
        /// </summary>
        public static class FolderBrowserDialog
        {
            delegate int BrowseCallBackProc(IntPtr hwnd, int msg, IntPtr lp, IntPtr wp);

            /// <summary>
            /// typedef struct _browseinfo {
            /// HWND              hwndOwner;
            /// PCIDLIST_ABSOLUTE pidlRoot;
            /// LPTSTR            pszDisplayName;
            /// LPCTSTR           lpszTitle;
            /// UINT              ulFlags;
            /// BFFCALLBACK       lpfn;
            /// LPARAM            lParam;
            /// int               iImage;
            /// }
            /// </summary>
            [StructLayout(LayoutKind.Sequential)]
            struct BROWSEINFO
            {
                /// <summary>
                /// A handle to the owner window for the dialog box.
                /// </summary>
                public IntPtr hwndOwner;
                /// <summary>
                /// A PIDL that specifies the location of the root folder from which to start browsing. 
                /// Only the specified folder and its subfolders in the namespace hierarchy appear in the dialog box. 
                /// This member can be NULL; in that case, the namespace root (the Desktop folder) is used.
                /// </summary>
                public IntPtr pidlRoot;
                /// <summary>
                /// Pointer to a buffer to receive the display name of the folder selected by the user. 
                /// The size of this buffer is assumed to be MAX_PATH characters.
                /// </summary>
                [MarshalAs(UnmanagedType.LPTStr)]
                public string pszDisplayName;
                /// <summary>
                /// Pointer to a null-terminated string that is displayed above the tree view control in the dialog box. 
                /// This string can be used to specify instructions to the user.
                /// </summary>
                [MarshalAs(UnmanagedType.LPStr)]
                public string lpszTitle;
                /// <summary>
                /// Flags that specify the options for the dialog box. 
                /// </summary>
                public uint ulFlags;
                /// <summary>
                /// Pointer to an application-defined function that the dialog box calls when an event occurs.
                /// </summary>
                public BrowseCallBackProc lpfn;
                /// <summary>
                /// An application-defined value that the dialog box passes to the callback function, if one is specified in lpfn.
                /// </summary>
                public IntPtr lParam;
                /// <summary>
                /// An integer value that receives the index of the image associated with the selected folder, stored in the system image list.
                /// </summary>
                public int iImage;
            }

            #region Constants

            // Constants for sending and receiving messages in BrowseCallBackProc
            public const int WM_USER = 0x400;
            public const int BFFM_INITIALIZED = 1;
            public const int BFFM_SELCHANGED = 2;
            public const int BFFM_VALIDATEFAILEDA = 3;
            public const int BFFM_VALIDATEFAILEDW = 4;
            public const int BFFM_IUNKNOWN = 5; // provides IUnknown to client. lParam: IUnknown*
            public const int BFFM_SETSTATUSTEXTA = WM_USER + 100;
            public const int BFFM_ENABLEOK = WM_USER + 101;
            public const int BFFM_SETSELECTIONA = WM_USER + 102;
            public const int BFFM_SETSELECTIONW = WM_USER + 103;
            public const int BFFM_SETSTATUSTEXTW = WM_USER + 104;
            public const int BFFM_SETOKTEXT = WM_USER + 105; // Unicode only
            public const int BFFM_SETEXPANDED = WM_USER + 106; // Unicode only

            // Browsing for directory.

            /// <summary>
            /// Only return file system directories. 
            /// If the user selects folders that are not part of the file system, the OK button is grayed. 
            /// </summary>
            private const uint BIF_RETURNONLYFSDIRS = 0x0001;  
            /// <summary>
            /// Do not include network folders below the domain level in the dialog box's tree view control.
            /// </summary>
            private const uint BIF_DONTGOBELOWDOMAIN = 0x0002; 
            /// <summary>
            /// Include a status area in the dialog box. 
            /// The callback function can set the status text by sending messages to the dialog box. 
            /// This flag is not supported when BIF_NEWDIALOGSTYLE is specified.
            /// </summary>
            private const uint BIF_STATUSTEXT = 0x0004;
            /// <summary>
            /// Only return file system ancestors. 
            /// An ancestor is a subfolder that is beneath the root folder in the namespace hierarchy. 
            /// If the user selects an ancestor of the root folder that is not part of the file system, the OK button is grayed.
            /// </summary>
            private const uint BIF_RETURNFSANCESTORS = 0x0008;
            /// <summary>
            /// Include an edit control in the browse dialog box that allows the user to type the name of an item.
            /// </summary>
            private const uint BIF_EDITBOX = 0x0010;
            /// <summary>
            /// If the user types an invalid name into the edit box, the browse dialog box calls the application's BrowseCallbackProc with the BFFM_VALIDATEFAILED message. 
            /// This flag is ignored if BIF_EDITBOX is not specified.
            /// </summary>
            private const uint BIF_VALIDATE = 0x0020;  
            /// <summary>
            /// Use the new user interface. 
            /// Setting this flag provides the user with a larger dialog box that can be resized. 
            /// The dialog box has several new capabilities, including: drag-and-drop capability within the dialog box, reordering, shortcut menus, new folders, delete, and other shortcut menu commands. 
            /// </summary>
            private const uint BIF_NEWDIALOGSTYLE = 0x0040;  
            /// <summary>
            /// Use the new user interface, including an edit box. 
            /// This flag is equivalent to BIF_EDITBOX | BIF_NEWDIALOGSTYLE. 
            /// If COM is initialized through CoInitializeEx with the COINIT_MULTITHREADED flag set, SHBrowseForFolder fails if BIF_USENEWUI is passed.
            /// </summary>
            private const uint BIF_USENEWUI = BIF_EDITBOX | BIF_NEWDIALOGSTYLE;
            /// <summary>
            /// The browse dialog box can display URLs. The BIF_USENEWUI and BIF_BROWSEINCLUDEFILES flags must also be set. If any of these three flags are not set, the browser dialog box rejects URLs. Even when these flags are set, the browse dialog box displays URLs only if the folder that contains the selected item supports URLs. When the folder's IShellFolder::GetAttributesOf method is called to request the selected item's attributes, the folder must set the SFGAO_FOLDER attribute flag. Otherwise, the browse dialog box will not display the URL.
            /// </summary>
            private const uint BIF_BROWSEINCLUDEURLS = 0x0080;  
            /// <summary>
            /// When combined with BIF_NEWDIALOGSTYLE, adds a usage hint to the dialog box, in place of the edit box. 
            /// BIF_EDITBOX overrides this flag.
            /// </summary>
            private const uint BIF_UAHINT = 0x0100;  
            /// <summary>
            /// Do not include the New Folder button in the browse dialog box.
            /// </summary>
            private const uint BIF_NONEWFOLDERBUTTON = 0x0200; 
            /// <summary>
            /// When the selected item is a shortcut, return the PIDL of the shortcut itself rather than its target.
            /// </summary>
            private const uint BIF_NOTRANSLATETARGETS = 0x0400; 
            /// <summary>
            /// Only return computers. If the user selects anything other than a computer, the OK button is grayed.
            /// </summary>
            private const uint BIF_BROWSEFORCOMPUTER = 0x1000; 
            /// <summary>
            /// Only allow the selection of printers. If the user selects anything other than a printer, the OK button is grayed. 
            /// </summary>
            private const uint BIF_BROWSEFORPRINTER = 0x2000;
            /// <summary>
            /// The browse dialog box displays files as well as folders.
            /// </summary>
            private const uint BIF_BROWSEINCLUDEFILES = 0x4000;
            /// <summary>
            /// The browse dialog box can display shareable resources on remote systems. This is intended for applications that want to expose remote shares on a local system. 
            /// The BIF_NEWDIALOGSTYLE flag must also be set.
            /// </summary>
            private const uint BIF_SHAREABLE = 0x8000;
            /// <summary>
            /// Windows 7 and later. Allow folder junctions such as a library or a compressed file with a .zip file name extension to be browsed.
            /// </summary>
            private const uint BIF_BROWSEFILEJUNCTIONS = 0x00010000;
            /// <summary>
            /// Maximum path length (256)
            /// </summary>
            private const int MAX_PATH = 256;

            #endregion

            #region Imports


            [DllImport("shell32.dll")]
            static extern IntPtr SHBrowseForFolder(ref BROWSEINFO lpbi);
            // The BROWSEINFO object's pszDisplayName only gives you the name of the folder.
            // To get the actual path, you need to parse the returned PIDL
            [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
            static extern bool SHGetPathFromIDList(IntPtr pidl, IntPtr pszPath);
            [DllImport("user32.dll", CharSet = CharSet.Auto)]
            static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam);
            [DllImport("user32.dll", CharSet = CharSet.Auto)]
            static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam);

            #endregion

            private static int OnBrowseEvent(IntPtr hWnd, int msg, IntPtr lp, IntPtr lpData)
            {
                switch (msg)
                {
                    case BFFM_INITIALIZED: // Required to set initialPath
                        {
                            SendMessage(hWnd, BFFM_SETSELECTIONW, 1, lpData);
                            break;
                        }

                    case BFFM_SELCHANGED:
                        {
                            IntPtr pathPtr = Marshal.AllocHGlobal((int)(260 * Marshal.SystemDefaultCharSize));
                            if (SHGetPathFromIDList(lp, pathPtr))
                                SendMessage(hWnd, BFFM_SETSTATUSTEXTW, 0, pathPtr);
                            Marshal.FreeHGlobal(pathPtr);
                            break;
                        }

                    case BFFM_VALIDATEFAILEDA:
                    case BFFM_VALIDATEFAILEDW:
                        return -1;
                }

                return 0;
            }

            /// <summary>
            /// Display a folder browser dialog
            /// </summary>
            /// <param name="parentHandle">A window handle</param>
            /// <param name="title">Optional description text</param>
            /// <returns>Selected path or null</returns>
            public static string SelectFolder(IntPtr parentHandle, string title = null)
            {
                var sb = new StringBuilder(MAX_PATH);
                IntPtr bufferAddress = Marshal.AllocHGlobal(MAX_PATH); ;
                IntPtr pidl = IntPtr.Zero;
                var bi = new BROWSEINFO
                             {
                                 hwndOwner = parentHandle,
                                 pidlRoot = IntPtr.Zero,
                                 pszDisplayName = null,
                                 lpszTitle =  title ,//?? "Select a folder",
                                 ulFlags = BIF_NEWDIALOGSTYLE | BIF_RETURNONLYFSDIRS | BIF_NONEWFOLDERBUTTON | BIF_UAHINT ,
                                 lpfn = new BrowseCallBackProc(OnBrowseEvent),
                                 lParam = IntPtr.Zero,
                                 iImage = 0
                             };

                try
                {
                    pidl = SHBrowseForFolder(ref bi);
                    if (SHGetPathFromIDList(pidl, bufferAddress))
                        sb.Append(Marshal.PtrToStringAuto(bufferAddress));
                }
                catch(Exception exp)
                {
                    Trace.TraceError(exp.Message);
                }
                finally
                {
                    // Caller is responsible for freeing this memory.
                    Marshal.FreeCoTaskMem(pidl);
                    Marshal.FreeHGlobal(bufferAddress);
                }

                return sb.Length > 0 ? sb.ToString() : null;
            }
        }
    }
}